// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';

import 'package:async/async.dart';

import '../../build_plan/package_graph.dart';
import '../../logging/build_log.dart';
import 'asset_change.dart';
import 'node_watcher.dart';

PackageNodeWatcher _default(PackageNode node) => PackageNodeWatcher(node);

/// Allows watching an entire graph of packages to schedule rebuilds.
class PackageGraphWatcher {
  final PackageNodeWatcher Function(PackageNode) _strategy;
  final PackageGraph _graph;

  final _readyCompleter = Completer<void>();
  Future<void> get ready => _readyCompleter.future;

  bool _isWatching = false;

  /// Creates a new watcher for a [PackageGraph].
  ///
  /// May optionally specify a [watch] strategy, otherwise will attempt a
  /// reasonable default based on the current platform.
  PackageGraphWatcher(
    this._graph, {
    PackageNodeWatcher Function(PackageNode node)? watch,
  }) : _strategy = watch ?? _default;

  /// Returns a stream of records for assets that changed in the package graph.
  Stream<AssetChange> watch() {
    assert(!_isWatching);
    _isWatching = true;
    return LazyStream(_watch);
  }

  Stream<AssetChange> _watch() {
    final allWatchers =
        _graph.allPackages.values
            .where((node) => node.dependencyType == DependencyType.path)
            .map(_strategy)
            .toList();
    final filteredEvents =
        allWatchers
            .map(
              (w) => w.watch().where(_nestedPathFilter(w.node)).handleError((
                Object e,
                StackTrace s,
              ) {
                buildLog.error(
                  buildLog.renderThrowable(
                    'Failed to watch files in package:${w.node.name}.',
                    e,
                  ),
                );
              }),
            )
            .toList();
    // Asynchronously complete the `_readyCompleter` once all the watchers
    // are done.
    () async {
      await Future.wait(
        allWatchers.map((nodeWatcher) => nodeWatcher.watcher.ready),
      );
      _readyCompleter.complete();
    }();
    return StreamGroup.merge(filteredEvents);
  }

  bool Function(AssetChange) _nestedPathFilter(PackageNode rootNode) {
    final ignorePaths = _nestedPaths(rootNode);
    return (change) => !ignorePaths.any(change.id.path.startsWith);
  }

  // Returns a set of all package paths that are "nested" within a node.
  //
  // This allows the watcher to optimize and avoid duplicate events.
  List<String> _nestedPaths(PackageNode rootNode) {
    return _graph.allPackages.values
        .where((node) {
          return node.path.length > rootNode.path.length &&
              node.path.startsWith(rootNode.path);
        })
        .map(
          (node) =>
              node.path.substring(rootNode.path.length + 1) +
              Platform.pathSeparator,
        )
        .toList();
  }
}
